综述
在 ES6 之前,ES5 实现面向对象是大家经常讨论的问题,趁着 ES6 还没进入浏览器,借我自己的一段脚本,跟大家讨论一下 js 面向对象的一些细节问题,欢迎留言指教。
例子代码
基本的概念比如“基类”,“子类”等就不解释了。下面是我写的一段实现类继承的 js 脚本:
/**
* @file Inheritable.js
* @author Y3G
* @fileoverview
* 可继承对象
*/
var _ = require('lodash');
var assert = require('./assert');
var seq = require('./seq');
/**
* 并联函数
* @return 生成的函数
*/
var parallel = function () {
var fns = _.filter(arguments, _.isFunction);
if (fns.length === 0) return;
return function () {
_.forEach(fns, function (fn) {
fn.apply(this, _.slice(arguments));
});
};
}
/**
* 创建类
* @param base 基类函数或原型对象
* @param ex 实例扩展
* @return 生成的类函数
* 默认方法:
* __init__: 构造函数
* __initProto__: 原型构造函数
* 默认类方法:
* makeClass: 创建类
* makeSubClass: 创建子类
*/
function makeClass(base, ex) {
base = base || {};
ex = ex || {};
var proto = _.isFunction(base) ? new base(true) : base;
ex.__init__ = parallel(proto.__init__, ex.__init__); // 实例初始化函数
ex.__initProto__ = parallel(proto.__initProto__, ex.__initProto__); // 原型初始化函数
proto = _.mixin(proto, ex); // 合并原型和实例扩展
function SubClass(isProto) {
var initFunc = isProto ? this.__initProto__ : this.__init__;
if (_.isFunction(initFunc)) {
initFunc.apply(this, _.slice(arguments));
}
this.$id = seq();
}
if (_.isFunction(base)) {
// 复制静态函数等属性到子类
_.forOwn(base, function (val, key) {
SubClass[key] = val;
});
}
SubClass.prototype = proto;
SubClass.prototype.constructor = SubClass;
SubClass.makeSubClass = _.curry(makeClass, SubClass);
SubClass.makeClass = makeClass;
return SubClass;
}
/**
* 根基类
* @class Root
*/
var Root = makeClass();
module.exports = Root;
这段代码导出了一个根基类 Root,它有一个静态方法叫做 makeSubClass,调用可生成一个 Root 的子类,创建出的子类同样带有 makeSubClass 这个静态方法。同时,创建的子类有几个固定字段,分别是:
__init__
初始化函数__initProto__
原型初始化函数$id
对象 id
通过 parallel 这个函数,makeClass 把基类和子类的 __init__
函数合并执行,这样解决了基类构造函数无法执行的问题。
下面说说我对几个细节问题的思考。
几个问题和我的看法
问:构造函数 __init__
的执行顺序是基类 -> 子类比较好还是子类 -> 基类比较好?
答:
按照我贴的代码,是基类的构造函数先执行,当时我是想模仿 C++。但是我现在认为应该子类构造函数先执行。
原因很简单,就是 ES6 使用的是类 java 方式, constructor 函数是子类先执行的,并且基类 constructor 是靠
super() 手工调用的。基于一点 java 的使用经验,我也认为这样的构造顺序,比基类 -> 子类灵活不少。另外,采用和 ES6 一样的构造顺序,更有利于移植。
问:__initProto__
是什么鬼?
答:
这是我一直以来坚持的看法——用 js 模拟类,如果一个类的实例有可能作为 prototype 存在,就必须把实例构造和 prototype
构造分开,而__initProto__
就是专门用来初始化 prototype 的。原因有两方面:
一是对象可能会很昂贵,占很多资源。
二是构造函数
__init__
可能不止会操作 this,还可能会修改全局的某些状态(比如计数器)。这种时候多创建一个和少创建一个实例,显然是不同的。
问:为什么要把基类函数上的静态内容都拷贝到子类函数上?
答:
因为原型链查找对静态内容无效。
比如这样:
var Foo = Root.makeSubClass({}); Foo.SOME_STATIC_THING = 0; var Bar = Foo.makeSubClass({}); alert(Bar.SOME_STATIC_THING);
这时候假设不拷贝
SOME_STATIC_THING
到子类上去,就不能通过Bar.SOME_STATIC_THING
访问到该属性。这是非常违反常识的,没有语言是这样子的。同时,现在又有了另一个这么做的理由,就是 ES6 有 static
关键字,如果你使用其他方式实现静态属性,将不利于以后移植到 ES6。不过这里有个小坑,就如果静态变量是基本类型(比如字符串),那么显然在子类上修改对基类无效。于是基本类型的静态变量只能是常量,如果需要非常量的静态变量,必须使用对象。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。